package com.ttxs.gateway.filter;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.ttxs.common.core.constant.Common;
import com.ttxs.common.core.constant.SecurityConstant;
import com.ttxs.common.core.properties.SecurityProperties;
import com.ttxs.common.core.properties.TokenProperties;
import com.ttxs.common.core.utils.ServletUtils;
import com.ttxs.common.core.redis.RedisService;
import com.ttxs.common.core.utils.AuthUtils;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

import javax.annotation.Resource;
import java.time.Duration;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.TimeUnit;

@Component
@Slf4j
public class AuthFilter implements GlobalFilter, Ordered {
    @Autowired
    private RedisService redisService;

    @Autowired
    private TokenProperties tokenProperties;

    @Resource
    private SecurityProperties securityProperties;

    @Value("${ttxs.demoEnable:false}")
    private boolean demoEnable;

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        ServerHttpRequest request = exchange.getRequest();
        ServerHttpResponse response = exchange.getResponse();
        ServerHttpRequest.Builder mutate = request.mutate();
        String path = request.getURI().getPath();
        String method = request.getMethodValue();

        log.info("请求：method:{}, uri:{}", method, path);
        // 只能通过网关访问微服务
        if(securityProperties.getSecretKeyEnabled()) {
            // 移除伪造的secretKey
            removeHeader(mutate, SecurityConstant.SECRET_KEY);
            // 添加secretKey
            String secretKey = createSecretKey();
            addHeader(mutate, SecurityConstant.SECRET_KEY, secretKey);
        }
        // 检查是否启用鉴权，不管是否启用，都要验证token
        if (!securityProperties.isEnableAuth()) {
            log.warn("path【{}】，method【{}】，系统已经关闭鉴权，直接放行", path, method);
            return chain.filter(exchange);
        }

        if(AuthUtils.contains(securityProperties.getWhiteList(), path)
                || securityProperties.getExcludes().contains(path)) {
            log.info("path【{}】无需鉴权，直接放行", path);
            return chain.filter(exchange);
        }

        if (demoEnable && !method.equals("GET")) {
            return error(response, HttpStatus.SERVICE_UNAVAILABLE, "演示模式不能修改数据");
        }
        log.info("path【{}】，method【{}】需要鉴权，网关调用鉴权api", path, method);
        String token = getToken(request);
        if(!StringUtils.hasText(token)) {
            log.warn("令牌为空");
            return error(response, HttpStatus.UNAUTHORIZED, "令牌为空");
        }
        if (!AuthUtils.isLogin(token)) {
            log.error("令牌无效");
            return error(response, HttpStatus.UNAUTHORIZED, "令牌无效");
        }
        // 检测是否有权限访问控制器
        String authority = String.join("_", method.toUpperCase(), path);
        if (!AuthUtils.hasPerms(authority, token)) {
            log.error(String.format("无权访问此api:%s,%s", method, path));
            return error(response, HttpStatus.FORBIDDEN, String.format("无权访问此api:%s,%s", method, path));
        }
        // 添加platformId到请求头
        addHeader(mutate, Common.HttpHeaders.PLATFORM_ID, AuthUtils.getLoginUser(token).getExtra().get(Common.HttpHeaders.PLATFORM_ID));
        log.info(String.format("路由到api:%s,%s", method, path));
        return chain.filter(exchange.mutate().request(mutate.build()).build());
    }

    @SneakyThrows
    public static Mono<Void> error(ServerHttpResponse response, HttpStatus status, String msg) {
        ObjectMapper objectMapper = new ObjectMapper();
        Map<String, Object> data = new HashMap<>();
        data.put("code", status.value());
        data.put("msg", "网关错误处理：" + msg);
        String json = objectMapper.writeValueAsString(data);
        return ServletUtils.webFluxResponseWriter(response, status, json);
    }

    private void addHeader(ServerHttpRequest.Builder mutate, String name, Object value) {
        if (value == null) {
            return;
        }
        String valueStr = value.toString();
        String valueEncode = ServletUtils.urlEncode(valueStr);
        mutate.header(name, valueEncode);
    }

    private void removeHeader(ServerHttpRequest.Builder mutate, String name) {
        mutate.headers(httpHeaders -> httpHeaders.remove(name)).build();
    }

    private String getToken(ServerHttpRequest request) {
        String token = request.getHeaders().getFirst(tokenProperties.getName());
        if(token == null) {
            // 从查询参数中获取token，支持非前后端分离应用集成
            token = request.getQueryParams().getFirst(tokenProperties.getName());
            // 存储token到请求头
            if(StringUtils.hasText(token)) {
                addHeader(request.mutate(), tokenProperties.getName(), token);
            }
        }
        return token;
    }

    private String createSecretKey() {
        String secretKey = null;
        if(redisService.hasKey(SecurityConstant.SECRET_KEY)) {
            if(redisService.getExpire(SecurityConstant.SECRET_KEY) < securityProperties.getSecretKeyLastTime()) {
                redisService.expire(SecurityConstant.SECRET_KEY, securityProperties.getSecretKeyExpiration(), TimeUnit.SECONDS);
            }
            secretKey = (String) redisService.get(SecurityConstant.SECRET_KEY);
        } else {
            secretKey = UUID.randomUUID().toString();
            redisService.set(SecurityConstant.SECRET_KEY, secretKey, Duration.ofSeconds(securityProperties.getSecretKeyExpiration()));
        }
        return secretKey;
    }

    @Override
    public int getOrder() {
        return 2;
    }
}
